/*
 Copyright (C) BABEC. All rights reserved.
 Copyright (C) THL A29 Limited, a Tencent company. All rights reserved.

 SPDX-License-Identifier: Apache-2.0
*/
/*
ERC20 Token Standard:
https://eips.ethereum.org/EIPS/eip-20
*/

package contract_demo_erc20

import (
	"chainmaker/pb/protogo"
	"chainmaker/safemath"
	"chainmaker/sdk"
	"errors"
	"fmt"
	"strconv"

	"chainmaker.org/chainmaker/common/v2/crypto"
)

type ERC20 interface {
	name() protogo.Response                                                                                    // return string
	symbol() protogo.Response                                                                                  // return string
	decimals() protogo.Response                                                                                // return string default "18"
	totalSupply(erc20Info *sdk.StoreMap) protogo.Response                                                      // return string
	balanceOf(account string) protogo.Response                                                                 // return string
	transfer(spender, to string, amount *safemath.SafeUint256) protogo.Response                                // return "transfer success"
	transferFrom(owner, spender, to string, amount *safemath.SafeUint256) protogo.Response                     // return "transfer success"
	approve(allowanceInfo *sdk.StoreMap, owner, spender string, amount *safemath.SafeUint256) protogo.Response // return "approve success"
	allowance(owner, spender string) protogo.Response                                                          // return string
}

type IERC20 interface {
	ERC20
	mint(account string, amount *safemath.SafeUint256) protogo.Response // return "mint success"
	// param name symbol decimals totalSupply
	InitContract() protogo.Response    // return "Init contract success"
	UpgradeContract() protogo.Response // return "upgrade contract success"
}

type ERC20Contract struct {
}

func (c *ERC20Contract) UpgradeContract() protogo.Response {
	return sdk.Success([]byte("upgrade contract success"))
}

func (c *ERC20Contract) InitContract() protogo.Response {
	args := sdk.Instance.GetArgs()
	if len(args) == 0 {
		return sdk.Error("erc20 contract's params should contains at least totalSupply param")
	}

	// name, symbol and decimal are optional
	name := args["name"]
	symbol := args["symbol"]
	decimalsStr := args["decimals"]
	totalSupplyStr := args["totalSupply"]
	erc20Info, err := sdk.NewStoreMap("erc20", 1, crypto.HASH_TYPE_SHA256)
	if err != nil {
		return sdk.Error(fmt.Sprintf("new storeMap of erc20Info failed, err:%s", err))
	}
	if len(name) > 0 {
		err = erc20Info.Set([]string{"name"}, name)
		if err != nil {
			return sdk.Error(fmt.Sprintf("set name of erc20Info failed, err:%s", err))
		}
	}
	if len(symbol) > 0 {
		err = erc20Info.Set([]string{"symbol"}, symbol)
		if err != nil {
			return sdk.Error(fmt.Sprintf("set symbol of erc20Info failed, err:%s", err))
		}
	}
	//decimals default to 18
	if len(decimalsStr) > 0 {
		_, err := strconv.Atoi(string(decimalsStr))
		if err != nil {
			return sdk.Error(fmt.Sprintf("param decimals err"))
		}
		err = erc20Info.Set([]string{"decimals"}, decimalsStr)
		if err != nil {
			return sdk.Error(fmt.Sprintf("set name of erc20Info failed, err:%s", err))
		}
	} else {
		err = erc20Info.Set([]string{"decimals"}, []byte("18"))
		if err != nil {
			return sdk.Error(fmt.Sprintf("set name of erc20Info failed, err:%s", err))
		}
	}
	//total supply default to zero
	var totalSupplyValue []byte
	if len(totalSupplyStr) > 0 {
		_, ok := safemath.ParseSafeUint256(string(totalSupplyStr))
		if !ok {
			return sdk.Error("param totalSupply err")
		}
		totalSupplyValue = totalSupplyStr
	} else {
		totalSupplyValue = []byte("0")
	}
	err = erc20Info.Set([]string{"totalSupply"}, totalSupplyValue)
	if err != nil {
		return sdk.Error("set total supply of erc20Info failed")
	}

	return sdk.Success([]byte("Init contract success"))
}

func (c *ERC20Contract) InvokeContract(method string) protogo.Response {
	args := sdk.Instance.GetArgs()
	if len(method) == 0 {
		return sdk.Error("method of param should not be empty")
	}
	erc20Info, err := sdk.NewStoreMap("erc20", 1, crypto.HASH_TYPE_SHA256)
	if err != nil {
		return sdk.Error(fmt.Sprintf("new storeMap of erc20Info failed, err:%s", err))
	}

	switch method {
	case "totalSupply":
		return c.totalSupply(erc20Info)
	case "balanceOf":
		account := string(args["account"])
		if len(account) == 0 {
			return sdk.Error("Param account should not be empty")
		}
		return c.balanceOf(account)
	case "transfer":
		spender, err := sdk.Instance.GetSenderAddr()
		if err != nil {
			return sdk.Error(fmt.Sprintf("Get sender address failed, err:%s", err))
		}
		to := string(args["to"])
		amountStr := string(args["amount"])
		amount, ok := safemath.ParseSafeUint256(amountStr)
		if !ok {
			return sdk.Error("Parse amount failed")
		}
		return c.transfer(spender, to, amount)
	case "transferFrom":
		spender, err := sdk.Instance.GetSenderAddr()
		if err != nil {
			return sdk.Error(fmt.Sprintf("Get sender address failed, err:%s", err))
		}
		amountStr := string(args["amount"])
		amount, ok := safemath.ParseSafeUint256(amountStr)
		if !ok {
			return sdk.Error("Parse amount failed")
		}
		owner := string(args["owner"])
		to := string(args["to"])
		return c.transferFrom(owner, spender, to, amount)
	case "approve":
		spender, err := sdk.Instance.GetSenderAddr()
		if err != nil {
			return sdk.Error(fmt.Sprintf("Get sender address failed, err:%s", err))
		}
		amountStr := string(args["amount"])
		amount, ok := safemath.ParseSafeUint256(amountStr)
		if !ok {
			return sdk.Error("Parse amount failed")
		}
		from := string(args["from"])

		allowanceInfo, err := sdk.NewStoreMap("allowanceInfo", 2, crypto.HASH_TYPE_SHA256)
		if err != nil {
			return sdk.Error(fmt.Sprintf("new storeMap of allowanceInfo failed, err:%s", err))
		}

		return c.approve(allowanceInfo, from, spender, amount)
	case "allowance":
		spender, err := sdk.Instance.GetSenderAddr()
		if err != nil {
			return sdk.Error(fmt.Sprintf("Get sender address failed, err:%s", err))
		}
		owner := string(args["owner"])
		return c.allowance(owner, spender)
	// below methods are optional
	case "name":
		return c.name()
	case "symbol":
		return c.symbol()
	case "decimals":
		return c.decimals()
	case "mint":
		account := string(args["account"])
		amountStr := string(args["amount"])
		amount, ok := safemath.ParseSafeUint256(amountStr)
		if !ok {
			return sdk.Error("Parse amount failed")
		}
		return c.mint(account, amount)
	default:
		return sdk.Error("Invalid method")
	}
}

func (c *ERC20Contract) totalSupply(erc20Info *sdk.StoreMap) protogo.Response {
	totalSupply, err := erc20Info.Get([]string{"totalSupply"})
	if err != nil {
		return sdk.Error(fmt.Sprintf("Get totalSupply failed, err:%s", err))
	}
	return sdk.Success(totalSupply)
}

func (c *ERC20Contract) balanceOf(account string) protogo.Response {
	balanceInfo, err := sdk.NewStoreMap("balanceInfo", 1, crypto.HASH_TYPE_SHA256)
	if err != nil {
		return sdk.Error(fmt.Sprintf("New storeMap of balanceInfo failed, err:%s", err))
	}

	balance, err := c.getBalance(balanceInfo, account)
	if err != nil {
		return sdk.Error(fmt.Sprintf("Get balance failed, err:%s", err))
	}
	return sdk.Success([]byte(balance.ToString()))
}

func (c *ERC20Contract) transfer(spender, to string, amount *safemath.SafeUint256) protogo.Response {
	zeroAddr := string([]byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0})
	if len(spender) != safemath.AddrLen {
		return sdk.Error("ERC20: transfer from the invalid address")
	}
	if spender == zeroAddr {
		return sdk.Error("ERC20: transfer from the zero address")
	}
	if len(to) != safemath.AddrLen {
		return sdk.Error("ERC20: transfer to the invalid address")
	}
	if to == zeroAddr {
		return sdk.Error("ERC20: transfer to the zero address")
	}
	balanceInfo, err := sdk.NewStoreMap("balanceInfo", 1, crypto.HASH_TYPE_SHA256)
	if err != nil {
		return sdk.Error(fmt.Sprintf("New storeMap of balanceInfo failed, err:%s", err))
	}

	fromBalance, err := c.getBalance(balanceInfo, spender)
	if err != nil {
		return sdk.Error(fmt.Sprintf("ERC20: get from balance failed, %s", err))
	}
	if fromBalance.Cmp(amount) < 0 {
		return sdk.Error("transfer amount exceeds balance")
	}
	fromLast, ok := safemath.SafeSub(fromBalance, amount)
	if !ok {
		return sdk.Error(fmt.Sprintf("From balance sub amount failed, err:%s", err))
	}
	err = c.setBalance(balanceInfo, spender, fromLast)
	if err != nil {
		return sdk.Error(fmt.Sprintf("Set balance of spender failed, err:%s", err))
	}

	toBalance, err := c.getBalance(balanceInfo, spender)
	if err != nil {
		return sdk.Error(fmt.Sprintf("ERC20: get from balance failed, %s", err))
	}
	toLast, ok := safemath.SafeAdd(toBalance, amount)
	if !ok {
		return sdk.Error(fmt.Sprintf("To balance add amount failed, err:%s", err))
	}
	err = c.setBalance(balanceInfo, to, toLast)
	if err != nil {
		return sdk.Error(fmt.Sprintf("Set balance of spender failed, err:%s", err))
	}

	sdk.Instance.EmitEvent("transfer", []string{spender, to, amount.ToString()})
	return sdk.Success([]byte("transfer success"))
}

/**
 * @dev See {IERC20-transferFrom}.
 *
 * Emits an {Approval} event indicating the updated allowance. This is not
 * required by the EIP. See the note at the beginning of {ERC20}.
 *
 * NOTE: Does not update the allowance if the current allowance
 * is the maximum `uint256`.
 *
 * Requirements:
 *
 * - `from` and `to` cannot be the zero address.
 * - `from` must have a balance of at least `amount`.
 * - the caller must have allowance for ``from``'s tokens of at least
 * `amount`.
 */
func (c *ERC20Contract) transferFrom(owner, spender, to string, amount *safemath.SafeUint256) protogo.Response {
	err := c.spendAllowance(owner, spender, amount)
	if err != nil {
		return sdk.Error(fmt.Sprintf("spend allowance failed, err:%s", err))
	}
	return c.transfer(owner, to, amount)
}

/**
 * @dev See {IERC20-approve}.
 *
 * NOTE: If `amount` is the maximum `uint256`, the allowance is not updated on
 * `transferFrom`. This is semantically equivalent to an infinite approval.
 *
 * Requirements:
 *
 * - `spender` cannot be the zero address.
 */
func (c *ERC20Contract) approve(allowanceInfo *sdk.StoreMap, owner, spender string, amount *safemath.SafeUint256) protogo.Response {
	zeroAddr := string([]byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0})
	if len(owner) != safemath.AddrLen {
		return sdk.Error("ERC20: approve from the invalid address")
	}
	if owner == zeroAddr {
		return sdk.Error("ERC20: approve from the zero address")
	}
	if len(spender) != safemath.AddrLen {
		return sdk.Error("ERC20: approve to the invalid address")
	}
	if spender == zeroAddr {
		return sdk.Error("ERC20: approve to the zero address")
	}

	err := c.setAllowance(allowanceInfo, owner, spender, amount)
	if err != nil {
		return sdk.Error(fmt.Sprintf("set allowance failed, err:%s", err))
	}

	sdk.Instance.EmitEvent("approve", []string{owner, spender, amount.ToString()})

	return sdk.Success([]byte("approve success"))
}

func (c *ERC20Contract) allowance(owner, spender string) protogo.Response {

	allowanceInfo, err := sdk.NewStoreMap("allowanceInfo", 2, crypto.HASH_TYPE_SHA256)
	if err != nil {
		return sdk.Error(fmt.Sprintf("new storeMap of allowanceInfo failed, err:%s", err))
	}

	currentAllowance, err := c.getAllowance(allowanceInfo, owner, spender)
	if err != nil {
		return sdk.Error(fmt.Sprintf("ERC20: get allowance failed, err:%s", err))
	}

	return sdk.Success([]byte(currentAllowance.ToString()))
}

func (c *ERC20Contract) name() protogo.Response {
	erc20Info, err := sdk.NewStoreMap("erc20", 1, crypto.HASH_TYPE_SHA256)
	if err != nil {
		return sdk.Error(fmt.Sprintf("new storeMap of erc20Info failed, err:%s", err))
	}
	name, err := erc20Info.Get([]string{"name"})
	if err != nil {
		return sdk.Error(fmt.Sprintf("get name from erc20Info failed, err:%s", err))
	}
	return sdk.Success(name)
}

func (c *ERC20Contract) symbol() protogo.Response {
	erc20Info, err := sdk.NewStoreMap("erc20", 1, crypto.HASH_TYPE_SHA256)
	if err != nil {
		return sdk.Error(fmt.Sprintf("new storeMap of erc20Info failed, err:%s", err))
	}
	symbol, err := erc20Info.Get([]string{"symbol"})
	if err != nil {
		return sdk.Error(fmt.Sprintf("get symbol from erc20Info failed, err:%s", err))
	}
	return sdk.Success(symbol)
}

func (c *ERC20Contract) decimals() protogo.Response {
	erc20Info, err := sdk.NewStoreMap("erc20", 1, crypto.HASH_TYPE_SHA256)
	if err != nil {
		return sdk.Error(fmt.Sprintf("new storeMap of erc20Info failed, err:%s", err))
	}
	decimals, err := erc20Info.Get([]string{"decimals"})
	if err != nil {
		return sdk.Error(fmt.Sprintf("get decimals from erc20Info failed, err:%s", err))
	}
	return sdk.Success(decimals)
}

/** Creates `amount` tokens and assigns them to `account`, increasing
 * the total supply.
 *
 * Emits a {Transfer} event with `from` set to the zero address.
 *
 * Requirements:
 *
 * - `account` cannot be the zero address.
 */
func (c *ERC20Contract) mint(account string, amount *safemath.SafeUint256) protogo.Response {
	zeroAddr := string([]byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0})
	if len(account) != safemath.AddrLen {
		return sdk.Error("ERC20: approve from the invalid address")
	}
	if account == zeroAddr {
		return sdk.Error("ERC20: approve from the zero address")
	}

	erc20Info, err := sdk.NewStoreMap("erc20", 1, crypto.HASH_TYPE_SHA256)
	if err != nil {
		return sdk.Error(fmt.Sprintf("new storeMap of erc20Info failed, err:%s", err))
	}

	totalSupplyBytes, err := erc20Info.Get([]string{"totalSupply"})
	if err != nil {
		return sdk.Error(fmt.Sprintf("get total supply from balance info failed, err:%s", err))
	}

	totalSupply, ok := safemath.ParseSafeUint256(string(totalSupplyBytes))
	if !ok {
		return sdk.Error("invalid total supply failed")
	}
	newTotalSupply, ok := safemath.SafeAdd(totalSupply, amount)
	if !ok {
		return sdk.Error("total supply overflow")
	}
	err = erc20Info.Set([]string{"totalSupply"}, []byte(newTotalSupply.ToString()))
	if err != nil {
		return sdk.Error(fmt.Sprintf("set total supply failed, err:%s", err))
	}

	balanceInfo, err := sdk.NewStoreMap("balanceInfo", 1, crypto.HASH_TYPE_SHA256)
	if err != nil {
		return sdk.Error(fmt.Sprintf("New storeMap of balanceInfo failed, err:%s", err))
	}
	balanceStr, err := balanceInfo.Get([]string{account})
	if err != nil {
		return sdk.Error(fmt.Sprintf("get balance from balanceInfo failed, err:%s", err))
	}
	balance, ok := safemath.ParseSafeUint256(string(balanceStr))
	if !ok {
		//return sdk.Error(fmt.Sprintf("invalid balance from balanceInfo, balance:%s", string(balanceStr)))
		balance, _ = safemath.ParseSafeUint256("0")
	}
	newBalance, ok := safemath.SafeAdd(balance, amount)
	if !ok {
		return sdk.Error("newBalance overflow")
	}
	err = balanceInfo.Set([]string{account}, []byte(newBalance.ToString()))
	if err != nil {
		return sdk.Error(fmt.Sprintf("set new balance to balanceInfo failed, err:%s", err))
	}

	sdk.Instance.EmitEvent("Transfer", []string{zeroAddr, account, amount.ToString()})

	return sdk.Success([]byte("mint success"))
}

func (c *ERC20Contract) getBalance(balanceInfo *sdk.StoreMap, account string) (balance *safemath.SafeUint256, err error) {
	balanceBytes, err := balanceInfo.Get([]string{account})
	if err != nil {
		return nil, fmt.Errorf("get balance failed, err:%s", err)
	}
	balance, ok := safemath.ParseSafeUint256(string(balanceBytes))
	if !ok {
		return nil, fmt.Errorf("balance bytes invalid")
	}

	return balance, nil
}

func (c *ERC20Contract) setBalance(balanceInfo *sdk.StoreMap, account string, value *safemath.SafeUint256) error {
	err := balanceInfo.Set([]string{account}, []byte(value.ToString()))
	if err != nil {
		return fmt.Errorf("set balance failed, err:%s", err)
	}

	return nil
}

func (c *ERC20Contract) getAllowance(allowanceInfo *sdk.StoreMap, owner, spender string) (
	allowance *safemath.SafeUint256, err error) {
	allowanceBytes, err := allowanceInfo.Get([]string{owner, spender})
	if err != nil {
		return nil, fmt.Errorf("get balance failed, err:%s", err)
	}
	allowance, ok := safemath.ParseSafeUint256(string(allowanceBytes))
	if !ok {
		return nil, fmt.Errorf("balance bytes invalid")
	}

	return allowance, nil
}

func (c *ERC20Contract) setAllowance(allowanceInfo *sdk.StoreMap, owner, spender string,
	allowance *safemath.SafeUint256) error {
	return allowanceInfo.Set([]string{owner, spender}, []byte(allowance.ToString()))
}

func (c *ERC20Contract) spendAllowance(owner, spender string, amount *safemath.SafeUint256) error {
	zeroAddr := string([]byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0})
	if len(owner) != safemath.AddrLen {
		return fmt.Errorf("ERC20: spendAllowance from the invalid address")
	}
	if owner == zeroAddr {
		return fmt.Errorf("ERC20: spendAllowance from the zero address")
	}
	if len(spender) != safemath.AddrLen {
		return fmt.Errorf("ERC20: spendAllowance invalid spender address")
	}
	if spender == zeroAddr {
		return fmt.Errorf("ERC20: spendAllowance to the zero address")
	}
	allowanceInfo, err := sdk.NewStoreMap("allowanceInfo", 2, crypto.HASH_TYPE_SHA256)
	if err != nil {
		return fmt.Errorf("new storeMap of allowanceInfo failed, err:%s", err)
	}

	currentAllowance, err := c.getAllowance(allowanceInfo, owner, spender)
	if err != nil {
		return fmt.Errorf("ERC20: get allowance failed, err:%s", err)
	}

	// Does not update the allowance amount in case of infinite allowance.
	if currentAllowance.IsMaxSafeUint256() {
		return nil
	}
	remainingAllowance, ok := safemath.SafeSub(currentAllowance, amount)
	if !ok {
		return fmt.Errorf("ERC20: insufficient allowance")
	}

	resp := c.approve(allowanceInfo, owner, spender, remainingAllowance)
	if resp.Status != sdk.OK {
		return errors.New(resp.Message)
	}
	return nil
}
